import os
import json
import logging
from datetime import datetime
import time
from typing import Callable, Optional

from hdc import Hdc
from action import ActionType, Action, SwipeAction, ClickAction, \
    InputTextAction, DragAction, DumpLayoutAction, ScreenCaptureAction, \
    KeyEventAction, KeyEventHomeAction, KeyEventBackAction

class ScenarioError(Exception):
    pass

class Scenario:
    """
    scenario
    """
    def __init__(self, scenario_cfg_path: str = None):
        """
        scenario
        :param enable: scenario enable
        :param id: scenario id
        :param name: scenario name
        :param stopped: scenario stopped or not
        :param description: scenario description
        :param test_time: scenario test time 
        :param iteration_count: scenario iteration count, default -1 is inifite
        :param iteration: action list per scenario iteration
        """

        self.path = scenario_cfg_path
        self.enable = True
        self.id = ""
        self.name = "base scenario"
        self.stopped = False
        self.description = ""
        self.test_time: int = -1  # seconds
        self.interval: float = 0.1
        self.iteration_interval: float = 0.1 # seconds
        self.iteration_count = -1
        self.iteration_cur_count = 0
        self.iteration: list[Action] = []
        self._on_scenario_start_callbacks: list[Optional[Callable[[any], None]]] = []
        self._on_scenario_stop_callbacks: list[Optional[Callable[[any], None]]] = []
        self._on_iter_start_callbacks: list[Optional[Callable[[any, any], None]]] = []
        self._on_iter_stop_callbacks: list[Optional[Callable[[any, any], None]]] = []
        self._on_action_start_callbacks: list[Optional[Callable[[any, any, any], None]]] = []
        self._on_action_stop_callbacks: list[Optional[Callable[[any, any, any], None]]] = []
        if scenario_cfg_path != None and scenario_cfg_path != "":
            self.init_from_cfg(scenario_cfg_path)

    def __init_iteration(self, config: dict) -> bool:
        iteration = config.get("iteration", {})
        if not iteration:
            raise ScenarioError("iteration is empty in scenario configuration")
        action_type_handler_map: dict[str, Action] = {
            ActionType.SWIPE.value: SwipeAction,
            ActionType.CLICK.value: ClickAction,
            ActionType.INPUT_TEXT.value: InputTextAction,
            ActionType.DRAG.value: DragAction,
            ActionType.DUMPLAYOUT.value: DumpLayoutAction,
            ActionType.SCREEN_CAPTURE.value: ScreenCaptureAction,
            ActionType.KEYEVENT.value: KeyEventAction,
            ActionType.KEYEVENT_HOME.value: KeyEventHomeAction,
            ActionType.KEYEVENT_BACK.value: KeyEventBackAction
        }
        for iter in iteration:
            action_type = iter.get("action")
            if action_type in action_type_handler_map:
                action_handler: Action = action_type_handler_map[action_type]()
                if action_handler is None:
                    raise ScenarioError(f"action handler [action type: {action_type}] is None")
                self.iteration.append(action_handler.set_config(iter))
                logging.debug(f"action: {action_handler.to_string()}")
            else:
                raise ScenarioError(f"Unknown action type: {action_type}")
        if len(self.iteration) == 0:
            raise ScenarioError("action is empty in scenario configuration")
        logging.debug(f"iteration: {self.iteration}")
        return True
    
    def __init_scenario(self, config: dict) -> bool:
        if "enable" not in config:
            raise ScenarioError("no enable field in scenario configuration")
        if "name" not in config:
            raise ScenarioError("no name field in scenario configuration")
        if "test_time" not in config:
            raise ScenarioError("no test_time field in scenario configuration")
        if "iteration_count" not in config:
            raise ScenarioError("no iteration_count field in scenario configuration")
        if "iteration" not in config:
            raise ScenarioError("no iteration field in scenario configuration")
        self.enable = config.get("enable", True)
        self.id = config.get("id", "")
        self.name = config.get("name", "")
        self.description = config.get("description", "")
        self.test_time = config.get("test_time", -1)
        self.interval = config.get("interval", 1000) / 1000.0
        self.iteration_interval = config.get("iteration_interval", 500) / 1000.0
        self.iteration_count = config.get("iteration_count", -1)
        return self.__init_iteration(config)
        
    def stop(self):
        self.stopped = True
        
    def init_from_cfg(self, scenario_cfg_path: str) -> bool:
        if not os.path.exists(scenario_cfg_path):
            raise ScenarioError(f"scenario file '{scenario_cfg_path}' not exist")
        content = None
        realpath = os.path.realpath(scenario_cfg_path)
        logging.debug(f"scenario config path: {scenario_cfg_path}, realpath: {realpath}")
        with open(realpath, "r", encoding="utf-8") as scenario_cfg_file:
            content = json.load(scenario_cfg_file)
        if content is None:
            raise ScenarioError("scenario config is empty")
        return self.__init_scenario(content)

    @staticmethod
    def is_test_time_over(test_seconds: int, start_time: datetime, end_time: datetime) -> bool:
        if test_seconds <= 0: # <= 0 means infinite
            return False
        return test_seconds < (end_time - start_time).total_seconds()

    @staticmethod
    def is_iteration_count_over(iteration_count: int, current_count: int) -> bool:
        if iteration_count <= 0: # <= 0 means infinite
            return False
        return iteration_count <= current_count

    def reg_scenario_start_callback(self, callback: Callable[[any], None]):
        """
        Register scenario start callback, append mode
        """
        self._on_scenario_start_callbacks.append(callback)

    def reg_scenario_stop_callback(self, callback: Callable[[any], None]):
        """
        Register scenario stop callback, append mode
        """
        self._on_scenario_stop_callbacks.append(callback)
    
    def reg_scenario_iteration_start_callback(self, callback: Callable[[any], None]):
        """
        Register scenario iteration start callback, append mode
        """
        self._on_iter_start_callbacks.append(callback)

    def reg_scenario_iteration_stop_callback(self, callback: Callable[[any], None]):
        """
        Register scenario iteration stop callback, append mode
        """
        self._on_iter_stop_callbacks.append(callback)
        
    def reg_scenario_iteration_action_start_callback(self, callback: Callable[[any], None]):
        """
        Register scenario iteration action start callback, append mode
        """
        self._on_action_start_callbacks.append(callback)

    def reg_scenario_iteration_action_stop_callback(self, callback: Callable[[any], None]):
        """
        Register scenario iteration action stop callback, append mode
        """
        self._on_action_stop_callbacks.append(callback)

    def unreg_scenario_start_callbacks(self):
        """
        Unregister all scenario start callbacks
        """
        self._on_scenario_start_callbacks.clear()

    def unreg_scenario_stop_callbacks(self):
        """
        Unregister all scenario stop callbacks
        """
        self._on_scenario_stop_callbacks.clear()

    def unreg_scenario_iteration_start_callbacks(self):
        """
        Unregister all scenario iteration start callbacks
        """
        self._on_iter_start_callbacks.clear()
    
    def unreg_scenario_iteration_stop_callbacks(self):
        """
        Unregister all scenario iteration stop callbacks
        """
        self._on_iter_stop_callbacks.clear()
        
    def unreg_scenario_iteration_action_start_callbacks(self):
        """
        Unregister all scenario iteration action start callbacks
        """
        self._on_action_start_callbacks.clear()
    
    def unreg_scenario_iteration_action_stop_callbacks(self):
        """
        Unregister all scenario iteration action stop callbacks
        """
        self._on_action_stop_callbacks.clear()

    def execute(self, hdc: Hdc, device_id: str = ""):
        logging.info(f"\n{self.to_string()}")
        if not self.enable:
            logging.info(f"Scenario [{self.name}] is disabled")
            return
        test_seconds = self.test_time
        iteration_count = self.iteration_count
        iteration_cur = 0
        start_time = datetime.now()

        # on_scenario_start callbacks
        for callback in self._on_scenario_start_callbacks:
            callback(self)

        # enter scenario iterations
        while (not Scenario.is_test_time_over(test_seconds, start_time, datetime.now())) and \
            (not Scenario.is_iteration_count_over(iteration_count, iteration_cur)):
            logging.info(f"[Scenario-{self.name}, Iteration{iteration_cur}] begin.")

            # on_iteration_start callback
            for callback in self._on_iter_start_callbacks:
                callback(self, self.iteration)      

            for action in self.iteration:   
                if self.stopped:
                    logging.info(f"[Scenario-{self.name}, Iteration-{iteration_cur}], Action-{action.name} stopped.")
                    return
                
                if action is None:
                    raise ScenarioError("action is None")
                
                # on_action_start callback
                for callback in self._on_action_start_callbacks:
                    callback(self, self.iteration, action)
                    
                logging.debug(f"[Scenario-{self.name}, Iteration-{iteration_cur}] execute action: {action.to_string()}")
                action.execute(hdc, device_id)
                
                # on_action_stop callback
                for callback in self._on_action_stop_callbacks:
                    callback(self, self.iteration, action)

            # on_iteration_stop callbacks
            for callback in self._on_iter_stop_callbacks:
                callback(self, self.iteration)   

            logging.info(f"[Scenario-{self.name}, Iteration-{iteration_cur}] end.")

            iteration_cur += 1
            
            time.sleep(self.iteration_interval)

        # on_scenario_stop callbacks
        for callback in self._on_scenario_stop_callbacks:
            callback(self)

        time.sleep(self.interval)

    def to_string(self):
        return f"""Scenario ID: {self.id}
        Path: {self.path}
        Enable: {self.enable}
        Name: {self.name}
        Description: {self.description}
        TestTime: {self.test_time} seconds
        Iteration Count: {self.iteration_count}
        """